D:\a\scloud-dns\scloud-dns\src\config.rs
Line | Count | Source |
1 | | //! Configuration types for scloud-dns |
2 | | //! |
3 | | //! This file contains Serde (Deserialize/Serialize) structs that map to the |
4 | | //! JSON configuration you provided. It includes helpers to load the config |
5 | | //! from a file and a light `validate()` method placeholder you can extend. |
6 | | |
7 | | use crate::exceptions::SCloudException; |
8 | | use anyhow::{Context, Result}; |
9 | | use serde::{Deserialize, Serialize}; |
10 | | use std::collections::HashSet; |
11 | | use std::fs; |
12 | | use std::path::Path; |
13 | | |
14 | | /// Top-level configuration |
15 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
16 | | pub struct Config { |
17 | | #[serde(default)] |
18 | | pub server: ServerConfig, |
19 | | |
20 | | #[serde(default)] |
21 | | pub workers: WorkersConfig, |
22 | | |
23 | | #[serde(default)] |
24 | | pub logging: LoggingConfig, |
25 | | |
26 | | #[serde(default)] |
27 | | pub metrics: MetricsConfig, |
28 | | |
29 | | #[serde(default)] |
30 | | pub admin: AdminConfig, |
31 | | |
32 | | #[serde(default)] |
33 | | pub acl: Vec<AclEntry>, |
34 | | |
35 | | #[serde(default)] |
36 | | pub listener: Vec<ListenerConfig>, |
37 | | |
38 | | #[serde(default)] |
39 | | pub doh: DohConfig, |
40 | | |
41 | | #[serde(default)] |
42 | | pub forwarder: Vec<ForwarderConfig>, |
43 | | |
44 | | #[serde(default)] |
45 | | pub root_hints: RootHintsConfig, |
46 | | |
47 | | #[serde(default)] |
48 | | pub cache: CacheConfig, |
49 | | |
50 | | #[serde(default)] |
51 | | pub recursion: RecursionConfig, |
52 | | |
53 | | #[serde(default)] |
54 | | pub ratelimit: RateLimitConfig, |
55 | | |
56 | | #[serde(default)] |
57 | | pub zone: Vec<ZoneConfig>, |
58 | | |
59 | | #[serde(default)] |
60 | | pub tsig_key: Vec<TsigKey>, |
61 | | |
62 | | #[serde(default)] |
63 | | pub axfr: AxfrConfig, |
64 | | |
65 | | #[serde(default)] |
66 | | pub dnssec: DnssecConfig, |
67 | | |
68 | | #[serde(default)] |
69 | | pub policy: PolicyConfig, |
70 | | |
71 | | #[serde(default)] |
72 | | pub amplification_mitigation: AmplificationMitigationConfig, |
73 | | |
74 | | #[serde(default)] |
75 | | pub tuning: TuningConfig, |
76 | | |
77 | | #[serde(default)] |
78 | | pub view: Vec<ViewConfig>, |
79 | | |
80 | | #[serde(default)] |
81 | | pub monitoring: MonitoringConfig, |
82 | | |
83 | | #[serde(default)] |
84 | | pub dynupdate: Vec<DynUpdateConfig>, |
85 | | |
86 | | #[serde(default)] |
87 | | pub limits: LimitsConfig, |
88 | | } |
89 | | |
90 | | impl Config { |
91 | | /// Load config from a JSON file path |
92 | 9 | pub fn from_file(path: &Path) -> Result<Self, SCloudException> { |
93 | 9 | let s = fs::read_to_string(path) |
94 | 9 | .with_context(|| format!0 ("reading config file {}", path0 .display0 ())) |
95 | 9 | .map_err(|_| SCloudException::SCLOUD_CONFIG_FILE_NOT_FOUND)?0 ; |
96 | 9 | let cfg: Config = serde_json::from_str(&s) |
97 | 9 | .context("parsing JSON config") |
98 | 9 | .map_err(|_| SCloudException::SCLOUD_CONFIG_IMPOSSIBLE_TO_PARSE_JSON)?0 ; |
99 | 9 | cfg.validate()?0 ; |
100 | 9 | Ok(cfg) |
101 | 9 | } |
102 | | |
103 | | /// Validation hook |
104 | 10 | pub fn validate(&self) -> Result<(), SCloudException> { |
105 | 24 | let acl_names10 : HashSet<&str>10 = self.acl.iter()10 .map10 (|a| a.name.as_str()).collect10 (); |
106 | 16 | let tsig_names10 : HashSet<&str>10 = self.tsig_key.iter()10 .map10 (|t| t.name.as_str()).collect10 (); |
107 | 10 | let _forwarder_names: HashSet<&str> = |
108 | 24 | self.forwarder.iter()10 .map10 (|f| f.name.as_str()).collect10 (); |
109 | | |
110 | 72 | let is_acl_ref_valid10 = |s: &str| -> bool { |
111 | 72 | if s.trim().is_empty() { |
112 | 0 | return false; |
113 | 72 | } |
114 | 72 | acl_names.contains(s) || s16 .contains16 ('/') |
115 | 72 | }; |
116 | | |
117 | 10 | if self.server.bind_port == 0 { |
118 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_SERVER_PORT); |
119 | 10 | } |
120 | 10 | if self.server.max_udp_payload == 0 || self.server.max_udp_payload > 65535 { |
121 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_MAX_UDP_PAYLOAD); |
122 | 10 | } |
123 | 10 | if self.tuning.max_label_length == 0 || self.tuning.max_label_length > 63 { |
124 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_DNS_LIMITS); |
125 | 10 | } |
126 | 10 | if self.tuning.max_domain_length == 0 || self.tuning.max_domain_length > 253 { |
127 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_DNS_LIMITS); |
128 | 10 | } |
129 | 10 | if self.limits.max_udp_packet_size == 0 || self.limits.max_udp_packet_size > 65535 { |
130 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_DNS_LIMITS); |
131 | 10 | } |
132 | | |
133 | 10 | let mut listener_names = HashSet::new(); |
134 | 24 | for l in &self.listener10 { |
135 | 24 | if l.name.trim().is_empty() { |
136 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_LISTENER); |
137 | 24 | } |
138 | 24 | if !listener_names.insert(l.name.as_str()) { |
139 | 0 | return Err(SCloudException::SCLOUD_CONFIG_DUPLICATE_LISTENER_NAME); |
140 | 24 | } |
141 | 24 | if l.port == 0 { |
142 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_LISTENER_PORT); |
143 | 24 | } |
144 | 24 | if l.protocols.is_empty() { |
145 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_LISTENER_PROTOCOLS); |
146 | 24 | } |
147 | 24 | if !l.acl.trim().is_empty() && !is_acl_ref_valid(&l.acl) { |
148 | 0 | return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE); |
149 | 24 | } |
150 | | |
151 | 24 | if l.enable_tls.unwrap_or(false) { |
152 | 8 | if l.tls_cert_path.as_deref().unwrap_or("").trim().is_empty() { |
153 | 0 | return Err(SCloudException::SCLOUD_CONFIG_TLS_MISSING_CERT); |
154 | 8 | } |
155 | 8 | if l.tls_key_path.as_deref().unwrap_or("").trim().is_empty() { |
156 | 0 | return Err(SCloudException::SCLOUD_CONFIG_TLS_MISSING_KEY); |
157 | 8 | } |
158 | 8 | if !l.protocols.iter().any(|p| matches!(p, Protocol::TCP)) { |
159 | 0 | return Err(SCloudException::SCLOUD_CONFIG_TLS_REQUIRES_TCP); |
160 | 8 | } |
161 | 16 | } |
162 | | } |
163 | | |
164 | 10 | if self.doh.enabled { |
165 | 8 | if self |
166 | 8 | .doh |
167 | 8 | .tls_cert_path |
168 | 8 | .as_deref() |
169 | 8 | .unwrap_or("") |
170 | 8 | .trim() |
171 | 8 | .is_empty() |
172 | | { |
173 | 0 | return Err(SCloudException::SCLOUD_CONFIG_TLS_MISSING_CERT); |
174 | 8 | } |
175 | 8 | if self |
176 | 8 | .doh |
177 | 8 | .tls_key_path |
178 | 8 | .as_deref() |
179 | 8 | .unwrap_or("") |
180 | 8 | .trim() |
181 | 8 | .is_empty() |
182 | | { |
183 | 0 | return Err(SCloudException::SCLOUD_CONFIG_TLS_MISSING_KEY); |
184 | 8 | } |
185 | 8 | if self.doh.paths.is_empty() { |
186 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_DOH); |
187 | 8 | } |
188 | 2 | } |
189 | | |
190 | 10 | if self.recursion.enabled { |
191 | 8 | if self.recursion.allowed_acl.trim().is_empty() { |
192 | 0 | return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE); |
193 | 8 | } |
194 | 8 | if !is_acl_ref_valid(&self.recursion.allowed_acl) { |
195 | 0 | return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE); |
196 | 8 | } |
197 | 2 | } |
198 | | |
199 | 10 | let mut fwd_names = HashSet::new(); |
200 | 24 | for f in &self.forwarder10 { |
201 | 24 | if f.name.trim().is_empty() { |
202 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_FORWARDER); |
203 | 24 | } |
204 | 24 | if !fwd_names.insert(f.name.as_str()) { |
205 | 0 | return Err(SCloudException::SCLOUD_CONFIG_DUPLICATE_FORWARDER_NAME); |
206 | 24 | } |
207 | 24 | if f.addresses.is_empty() { |
208 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_FORWARDER); |
209 | 24 | } |
210 | 40 | for a in &f.addresses24 { |
211 | 40 | if a.parse::<std::net::SocketAddr>().is_err() { |
212 | 0 | return Err(SCloudException::SCLOUD_CONFIG_IMPOSSIBLE_TO_PARSE_ADDR); |
213 | 40 | } |
214 | | } |
215 | | } |
216 | | |
217 | 10 | let mut zone_names = HashSet::new(); |
218 | 32 | for z in &self.zone10 { |
219 | 32 | if z.name.trim().is_empty() { |
220 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_ZONE); |
221 | 32 | } |
222 | 32 | if !zone_names.insert(z.name.as_str()) { |
223 | 0 | return Err(SCloudException::SCLOUD_CONFIG_DUPLICATE_ZONE_NAME); |
224 | 32 | } |
225 | | |
226 | 32 | match z.kind { |
227 | | ZoneType::Master => { |
228 | 16 | let inline = z.inline.unwrap_or(false); |
229 | 16 | if inline { |
230 | 8 | if z.records.is_empty() { |
231 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_INLINE_ZONE); |
232 | 8 | } |
233 | 8 | let has_soa = z |
234 | 8 | .records |
235 | 8 | .iter() |
236 | 8 | .any(|r| r.r#type.eq_ignore_ascii_case("SOA")); |
237 | 8 | if !has_soa { |
238 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_INLINE_ZONE); |
239 | 8 | } |
240 | | } else { |
241 | 8 | if z.file.as_deref().unwrap_or("").trim().is_empty() { |
242 | 0 | return Err(SCloudException::SCLOUD_CONFIG_ZONE_MISSING_FILE); |
243 | 8 | } |
244 | | } |
245 | | |
246 | 16 | if let Some(acl8 ) = z.notify_acl.as_deref() { |
247 | 8 | if !acl.trim().is_empty() && !is_acl_ref_valid(acl) { |
248 | 0 | return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE); |
249 | 8 | } |
250 | 8 | } |
251 | 16 | if let Some(acl8 ) = z.allow_transfer_acl.as_deref() { |
252 | 8 | if !acl.trim().is_empty() && !is_acl_ref_valid(acl) { |
253 | 0 | return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE); |
254 | 8 | } |
255 | 8 | } |
256 | | |
257 | 16 | if let Some(k8 ) = z.axfr_tsig_key.as_deref() { |
258 | 8 | if !k.trim().is_empty() && !tsig_names.contains(k) { |
259 | 0 | return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_TSIG_KEY); |
260 | 8 | } |
261 | 8 | } |
262 | | } |
263 | | ZoneType::Slave => { |
264 | 8 | if z.masters.is_empty() { |
265 | 0 | return Err(SCloudException::SCLOUD_CONFIG_SLAVE_MISSING_MASTERS); |
266 | 8 | } |
267 | 8 | for m in &z.masters { |
268 | 8 | if m.parse::<std::net::SocketAddr>().is_err() { |
269 | 0 | return Err(SCloudException::SCLOUD_CONFIG_IMPOSSIBLE_TO_PARSE_ADDR); |
270 | 8 | } |
271 | | } |
272 | 8 | if z.file.as_deref().unwrap_or("").trim().is_empty() { |
273 | 0 | return Err(SCloudException::SCLOUD_CONFIG_ZONE_MISSING_FILE); |
274 | 8 | } |
275 | | } |
276 | | ZoneType::Forward => { |
277 | 8 | if z.forwarders.is_empty() { |
278 | 0 | return Err(SCloudException::SCLOUD_CONFIG_FORWARD_ZONE_MISSING_FORWARDERS); |
279 | 8 | } |
280 | 8 | for f in &z.forwarders { |
281 | 8 | if f.parse::<std::net::SocketAddr>().is_err() { |
282 | 0 | return Err(SCloudException::SCLOUD_CONFIG_IMPOSSIBLE_TO_PARSE_ADDR); |
283 | 8 | } |
284 | | } |
285 | | } |
286 | 0 | ZoneType::Stub => { |
287 | 0 | // TODO: not defined JSON yet, strict checks later when I will implement it. |
288 | 0 | } |
289 | | } |
290 | | |
291 | 48 | for r in &z.records32 { |
292 | 48 | if r.r#type.eq_ignore_ascii_case("MX") { |
293 | 8 | if r.priority.is_none() { |
294 | 0 | return Err(SCloudException::SCLOUD_CONFIG_MX_MISSING_PRIORITY); |
295 | 8 | } |
296 | 40 | } else if r.priority.is_some() { |
297 | 0 | return Err(SCloudException::SCLOUD_CONFIG_PRIORITY_ON_NON_MX); |
298 | 40 | } |
299 | | } |
300 | | } |
301 | | |
302 | 10 | let mut view_names = HashSet::new(); |
303 | 16 | for v in &self.view10 { |
304 | 16 | if v.name.trim().is_empty() { |
305 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_VIEW); |
306 | 16 | } |
307 | 16 | if !view_names.insert(v.name.as_str()) { |
308 | 0 | return Err(SCloudException::SCLOUD_CONFIG_DUPLICATE_VIEW_NAME); |
309 | 16 | } |
310 | 16 | if v.acl.trim().is_empty() || !is_acl_ref_valid(&v.acl) { |
311 | 0 | return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE); |
312 | 16 | } |
313 | 16 | for vz in &v.zones { |
314 | 16 | if vz.name.trim().is_empty() || vz.file.trim().is_empty() { |
315 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_VIEW); |
316 | 16 | } |
317 | | } |
318 | | } |
319 | | |
320 | 10 | for d8 in &self.dynupdate { |
321 | 8 | if d.zone.trim().is_empty() { |
322 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_DYNUPDATE); |
323 | 8 | } |
324 | 8 | if d.acl.trim().is_empty() || !is_acl_ref_valid(&d.acl) { |
325 | 0 | return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE); |
326 | 8 | } |
327 | 8 | if let Some(k) = d.tsig_key.as_deref() { |
328 | 8 | if !k.trim().is_empty() && !tsig_names.contains(k) { |
329 | 0 | return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_TSIG_KEY); |
330 | 8 | } |
331 | 0 | } |
332 | | |
333 | 8 | if !zone_names.contains(d.zone.as_str()) { |
334 | 0 | return Err(SCloudException::SCLOUD_CONFIG_DYNUPDATE_UNKNOWN_ZONE); |
335 | 8 | } |
336 | | } |
337 | | |
338 | 10 | Ok(()) |
339 | 10 | } |
340 | | |
341 | | /// Get the address of a specific forwarder by index value |
342 | | #[allow(unused)] |
343 | 5 | pub(crate) fn try_get_forwarder_addr_by_index( |
344 | 5 | &self, |
345 | 5 | forwarder_index: usize, |
346 | 5 | address_index: usize, |
347 | 5 | ) -> Result<std::net::SocketAddr, SCloudException> { |
348 | 5 | let addr = self |
349 | 5 | .forwarder |
350 | 5 | .get(forwarder_index) |
351 | 5 | .ok_or(SCloudException::SCLOUD_CONFIG_MISSING_FORWARDER)?0 |
352 | | .addresses |
353 | 5 | .get(address_index) |
354 | 5 | .ok_or(SCloudException::SCLOUD_CONFIG_MISSING_ADDRESS)?0 |
355 | 5 | .parse() |
356 | 5 | .map_err(|_| SCloudException::SCLOUD_CONFIG_IMPOSSIBLE_TO_PARSE_ADDR)?0 ; |
357 | | |
358 | 5 | Ok(addr) |
359 | 5 | } |
360 | | |
361 | | // TODO: add a loop to test the next address for each retry |
362 | 5 | pub(crate) fn try_get_forwarder_addr_by_name( |
363 | 5 | &self, |
364 | 5 | forwarder_name: &str, |
365 | 5 | ) -> Result<std::net::SocketAddr, SCloudException> { |
366 | 5 | let forwarder = self |
367 | 5 | .forwarder |
368 | 5 | .iter() |
369 | 12 | .find5 (|f| f.name == forwarder_name) |
370 | 5 | .ok_or(SCloudException::SCLOUD_CONFIG_MISSING_FORWARDER)?0 ; |
371 | | |
372 | 5 | for addr_str in &forwarder.addresses { |
373 | 5 | if let Ok(addr) = addr_str.parse::<std::net::SocketAddr>() { |
374 | 5 | return Ok(addr); |
375 | 0 | } |
376 | | } |
377 | | |
378 | 0 | Err(SCloudException::SCLOUD_CONFIG_IMPOSSIBLE_TO_PARSE_ADDR) |
379 | 5 | } |
380 | | } |
381 | | |
382 | | impl Default for Config { |
383 | 4 | fn default() -> Self { |
384 | 4 | Self { |
385 | 4 | server: ServerConfig::default(), |
386 | 4 | workers: WorkersConfig::default(), |
387 | 4 | logging: LoggingConfig::default(), |
388 | 4 | metrics: MetricsConfig::default(), |
389 | 4 | admin: AdminConfig::default(), |
390 | 4 | acl: Vec::new(), |
391 | 4 | listener: Vec::new(), |
392 | 4 | doh: DohConfig::default(), |
393 | 4 | forwarder: Vec::new(), |
394 | 4 | root_hints: RootHintsConfig::default(), |
395 | 4 | cache: CacheConfig::default(), |
396 | 4 | recursion: RecursionConfig::default(), |
397 | 4 | ratelimit: RateLimitConfig::default(), |
398 | 4 | zone: Vec::new(), |
399 | 4 | tsig_key: Vec::new(), |
400 | 4 | axfr: AxfrConfig::default(), |
401 | 4 | dnssec: DnssecConfig::default(), |
402 | 4 | policy: PolicyConfig::default(), |
403 | 4 | amplification_mitigation: AmplificationMitigationConfig::default(), |
404 | 4 | tuning: TuningConfig::default(), |
405 | 4 | view: Vec::new(), |
406 | 4 | monitoring: MonitoringConfig::default(), |
407 | 4 | dynupdate: Vec::new(), |
408 | 4 | limits: LimitsConfig::default(), |
409 | 4 | } |
410 | 4 | } |
411 | | } |
412 | | |
413 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
414 | | pub struct ServerConfig { |
415 | | pub name: String, |
416 | | pub environment: String, |
417 | | pub max_concurrent_requests: usize, |
418 | | pub graceful_shutdown_timeout_secs: u64, |
419 | | |
420 | | pub default_ttl: u32, |
421 | | pub max_udp_payload: usize, |
422 | | pub enable_edns: bool, |
423 | | pub enable_tcp: bool, |
424 | | pub enable_dnssec: bool, |
425 | | |
426 | | pub bind_port: u16, |
427 | | } |
428 | | |
429 | | impl Default for ServerConfig { |
430 | 5 | fn default() -> Self { |
431 | 5 | ServerConfig { |
432 | 5 | name: "scloud-dns".to_string(), |
433 | 5 | environment: "production".to_string(), |
434 | 5 | max_concurrent_requests: 5000, |
435 | 5 | graceful_shutdown_timeout_secs: 15, |
436 | 5 | default_ttl: 3600, |
437 | 5 | max_udp_payload: 4096, |
438 | 5 | enable_edns: true, |
439 | 5 | enable_tcp: true, |
440 | 5 | enable_dnssec: false, |
441 | 5 | bind_port: 53, |
442 | 5 | } |
443 | 5 | } |
444 | | } |
445 | | |
446 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
447 | | pub struct WorkersConfig { |
448 | | pub listener: u16, |
449 | | pub decoder: u16, |
450 | | pub query_dispatcher: u16, |
451 | | pub cache_lookup: u16, |
452 | | pub zone_manager: u16, |
453 | | pub resolver: u16, |
454 | | pub cache_writer: u16, |
455 | | pub encoder: u16, |
456 | | pub sender: u16, |
457 | | pub cache_janitor: u16, |
458 | | pub metrics: u16, |
459 | | pub tcp_acceptor: u16, |
460 | | } |
461 | | |
462 | | impl Default for WorkersConfig { |
463 | 4 | fn default() -> Self { |
464 | 4 | WorkersConfig { |
465 | 4 | listener: 5, |
466 | 4 | decoder: 5, |
467 | 4 | query_dispatcher: 3, |
468 | 4 | cache_lookup: 3, |
469 | 4 | zone_manager: 1, |
470 | 4 | resolver: 5, |
471 | 4 | cache_writer: 1, |
472 | 4 | encoder: 5, |
473 | 4 | sender: 5, |
474 | 4 | cache_janitor: 1, |
475 | 4 | metrics: 2, |
476 | 4 | tcp_acceptor: 1, |
477 | 4 | } |
478 | 4 | } |
479 | | } |
480 | | |
481 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
482 | | pub struct LoggingConfig { |
483 | | pub level: LogLevel, |
484 | | pub format: LogFormat, |
485 | | pub file: String, |
486 | | pub rotate: bool, |
487 | | pub live_print: bool, |
488 | | pub max_size_mb: u64, |
489 | | } |
490 | | |
491 | | impl Default for LoggingConfig { |
492 | 4 | fn default() -> Self { |
493 | 4 | LoggingConfig { |
494 | 4 | level: LogLevel::INFO, |
495 | 4 | format: LogFormat::TEXT, |
496 | 4 | file: "/var/log/scloud-dns/scloud-dns.log".to_string(), |
497 | 4 | rotate: true, |
498 | 4 | live_print: false, |
499 | 4 | max_size_mb: 200, |
500 | 4 | } |
501 | 4 | } |
502 | | } |
503 | | |
504 | | #[allow(non_camel_case_types)] |
505 | | #[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] |
506 | | #[serde(rename_all = "lowercase")] |
507 | | pub enum LogLevel { |
508 | | TRACE = 0, |
509 | | DEBUG = 1, |
510 | | INFO = 2, |
511 | | WARN = 3, |
512 | | ERROR = 4, |
513 | | FATAL = 5, |
514 | | } |
515 | | |
516 | | impl LogLevel { |
517 | 0 | pub fn parse(s: &str) -> Self { |
518 | 0 | match s.to_ascii_lowercase().as_str() { |
519 | 0 | "trace" => Self::TRACE, |
520 | 0 | "debug" => Self::DEBUG, |
521 | 0 | "info" => Self::INFO, |
522 | 0 | "warn" | "warning" => Self::WARN, |
523 | 0 | "error" => Self::ERROR, |
524 | 0 | "fatal" => Self::FATAL, |
525 | 0 | _ => Self::WARN, |
526 | | } |
527 | 0 | } |
528 | | |
529 | 0 | pub(crate) fn as_str(self) -> &'static str { |
530 | 0 | match self { |
531 | 0 | Self::TRACE => "trace", |
532 | 0 | Self::DEBUG => "debug", |
533 | 0 | Self::INFO => "info", |
534 | 0 | Self::WARN => "warn", |
535 | 0 | Self::ERROR => "error", |
536 | 0 | Self::FATAL => "fatal", |
537 | | } |
538 | 0 | } |
539 | | } |
540 | | |
541 | | #[allow(non_camel_case_types)] |
542 | | #[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq)] |
543 | | #[serde(rename_all = "lowercase")] |
544 | | pub enum LogFormat { |
545 | | JSON, |
546 | | TEXT, |
547 | | } |
548 | | |
549 | | impl LogFormat { |
550 | 0 | pub fn parse(s: &str) -> Self { |
551 | 0 | match s.to_ascii_lowercase().as_str() { |
552 | 0 | "json" => Self::JSON, |
553 | 0 | _ => Self::TEXT, |
554 | | } |
555 | 0 | } |
556 | | } |
557 | | |
558 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
559 | | pub struct MetricsConfig { |
560 | | pub enabled: bool, |
561 | | pub prometheus_bind: String, |
562 | | pub enable_health_endpoint: bool, |
563 | | pub health_bind: String, |
564 | | } |
565 | | |
566 | | impl Default for MetricsConfig { |
567 | 4 | fn default() -> Self { |
568 | 4 | MetricsConfig { |
569 | 4 | enabled: true, |
570 | 4 | prometheus_bind: "0.0.0.0:9153".to_string(), |
571 | 4 | enable_health_endpoint: true, |
572 | 4 | health_bind: "127.0.0.1:8081".to_string(), |
573 | 4 | } |
574 | 4 | } |
575 | | } |
576 | | |
577 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
578 | | pub struct AdminConfig { |
579 | | pub enabled: bool, |
580 | | pub bind: String, |
581 | | pub auth_token: String, |
582 | | pub enable_tls: bool, |
583 | | } |
584 | | |
585 | | impl Default for AdminConfig { |
586 | 4 | fn default() -> Self { |
587 | 4 | AdminConfig { |
588 | 4 | enabled: true, |
589 | 4 | bind: "127.0.0.1:8053".to_string(), |
590 | 4 | auth_token: "replace-with-secure-token".to_string(), |
591 | 4 | enable_tls: false, |
592 | 4 | } |
593 | 4 | } |
594 | | } |
595 | | |
596 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
597 | | pub struct AclEntry { |
598 | | pub name: String, |
599 | | pub networks: Vec<String>, // CIDRs or single IPs; parse later with ipnet or similar |
600 | | } |
601 | | |
602 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
603 | | pub struct ListenerConfig { |
604 | | pub name: String, |
605 | | pub address: String, |
606 | | pub port: u16, |
607 | | #[serde(default)] |
608 | | pub protocols: Vec<Protocol>, |
609 | | #[serde(default)] |
610 | | pub recursion_allowed: bool, |
611 | | /// ACL name or a raw CIDR/list string |
612 | | #[serde(default)] |
613 | | pub acl: String, |
614 | | #[serde(default)] |
615 | | pub workers: Option<usize>, |
616 | | #[serde(default)] |
617 | | pub enable_tls: Option<bool>, |
618 | | #[serde(default)] |
619 | | pub tls_cert_path: Option<String>, |
620 | | #[serde(default)] |
621 | | pub tls_key_path: Option<String>, |
622 | | } |
623 | | |
624 | | impl Default for ListenerConfig { |
625 | 1 | fn default() -> Self { |
626 | 1 | ListenerConfig { |
627 | 1 | name: String::new(), |
628 | 1 | address: "0.0.0.0".to_string(), |
629 | 1 | port: 53, |
630 | 1 | protocols: vec![Protocol::UDP], |
631 | 1 | recursion_allowed: false, |
632 | 1 | acl: "0.0.0.0/0".to_string(), |
633 | 1 | workers: None, |
634 | 1 | enable_tls: None, |
635 | 1 | tls_cert_path: None, |
636 | 1 | tls_key_path: None, |
637 | 1 | } |
638 | 1 | } |
639 | | } |
640 | | |
641 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
642 | | #[serde(rename_all = "lowercase")] |
643 | | pub enum Protocol { |
644 | | UDP, |
645 | | TCP, |
646 | | } |
647 | | |
648 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
649 | | pub struct DohConfig { |
650 | | pub enabled: bool, |
651 | | pub bind: String, |
652 | | #[serde(default)] |
653 | | pub tls_cert_path: Option<String>, |
654 | | #[serde(default)] |
655 | | pub tls_key_path: Option<String>, |
656 | | #[serde(default)] |
657 | | pub paths: Vec<String>, |
658 | | #[serde(default)] |
659 | | pub allowed_origins: Vec<String>, |
660 | | } |
661 | | |
662 | | impl Default for DohConfig { |
663 | 5 | fn default() -> Self { |
664 | 5 | DohConfig { |
665 | 5 | enabled: false, |
666 | 5 | bind: "0.0.0.0:443".to_string(), |
667 | 5 | tls_cert_path: None, |
668 | 5 | tls_key_path: None, |
669 | 5 | paths: vec!["/dns-query".to_string()], |
670 | 5 | allowed_origins: Vec::new(), |
671 | 5 | } |
672 | 5 | } |
673 | | } |
674 | | |
675 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
676 | | pub struct ForwarderConfig { |
677 | | pub name: String, |
678 | | pub addresses: Vec<String>, |
679 | | pub policy: ForwardPolicy, |
680 | | pub timeout_ms: u64, |
681 | | pub edns: bool, |
682 | | pub use_tcp_on_retry: Option<bool>, |
683 | | } |
684 | | |
685 | | impl Default for ForwarderConfig { |
686 | 1 | fn default() -> Self { |
687 | 1 | ForwarderConfig { |
688 | 1 | name: String::new(), |
689 | 1 | addresses: Vec::new(), |
690 | 1 | policy: ForwardPolicy::First, |
691 | 1 | timeout_ms: 1500, |
692 | 1 | edns: true, |
693 | 1 | use_tcp_on_retry: Some(true), |
694 | 1 | } |
695 | 1 | } |
696 | | } |
697 | | |
698 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
699 | | #[serde(rename_all = "snake_case")] |
700 | | #[derive(PartialEq)] |
701 | | pub enum ForwardPolicy { |
702 | | RoundRobin, |
703 | | First, |
704 | | Random, |
705 | | } |
706 | | |
707 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
708 | | pub struct RootHintsConfig { |
709 | | pub file: String, |
710 | | } |
711 | | |
712 | | impl Default for RootHintsConfig { |
713 | 4 | fn default() -> Self { |
714 | 4 | RootHintsConfig { |
715 | 4 | file: "/etc/scloud/root.hints".to_string(), |
716 | 4 | } |
717 | 4 | } |
718 | | } |
719 | | |
720 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
721 | | pub struct CacheConfig { |
722 | | pub enabled: bool, |
723 | | pub max_entries: usize, |
724 | | pub max_ttl_seconds: u64, |
725 | | pub negative_ttl_seconds: u64, |
726 | | pub eviction_policy: String, |
727 | | } |
728 | | |
729 | | impl Default for CacheConfig { |
730 | 5 | fn default() -> Self { |
731 | 5 | CacheConfig { |
732 | 5 | enabled: true, |
733 | 5 | max_entries: 200_000, |
734 | 5 | max_ttl_seconds: 86_400, |
735 | 5 | negative_ttl_seconds: 300, |
736 | 5 | eviction_policy: "lru".to_string(), |
737 | 5 | } |
738 | 5 | } |
739 | | } |
740 | | |
741 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
742 | | pub struct RecursionConfig { |
743 | | pub enabled: bool, |
744 | | pub allowed_acl: String, |
745 | | pub max_recursive_queries: usize, |
746 | | pub recursion_timeout_ms: u64, |
747 | | pub retry_interval_ms: u64, |
748 | | } |
749 | | |
750 | | impl Default for RecursionConfig { |
751 | 5 | fn default() -> Self { |
752 | 5 | RecursionConfig { |
753 | 5 | enabled: false, |
754 | 5 | allowed_acl: "internal".to_string(), |
755 | 5 | max_recursive_queries: 50, |
756 | 5 | recursion_timeout_ms: 5000, |
757 | 5 | retry_interval_ms: 200, |
758 | 5 | } |
759 | 5 | } |
760 | | } |
761 | | |
762 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
763 | | pub struct RateLimitConfig { |
764 | | pub enabled: bool, |
765 | | pub global_qps: u64, |
766 | | pub per_ip_qps: u64, |
767 | | pub per_subnet_qps: u64, |
768 | | pub rrl: RrlConfig, |
769 | | } |
770 | | |
771 | | impl Default for RateLimitConfig { |
772 | 5 | fn default() -> Self { |
773 | 5 | RateLimitConfig { |
774 | 5 | enabled: true, |
775 | 5 | global_qps: 3000, |
776 | 5 | per_ip_qps: 100, |
777 | 5 | per_subnet_qps: 1000, |
778 | 5 | rrl: RrlConfig::default(), |
779 | 5 | } |
780 | 5 | } |
781 | | } |
782 | | |
783 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
784 | | pub struct RrlConfig { |
785 | | pub enabled: bool, |
786 | | pub window_seconds: u64, |
787 | | pub slip: u32, |
788 | | pub qps_threshold: u64, |
789 | | } |
790 | | |
791 | | impl Default for RrlConfig { |
792 | 5 | fn default() -> Self { |
793 | 5 | RrlConfig { |
794 | 5 | enabled: true, |
795 | 5 | window_seconds: 5, |
796 | 5 | slip: 2, |
797 | 5 | qps_threshold: 50, |
798 | 5 | } |
799 | 5 | } |
800 | | } |
801 | | |
802 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
803 | | pub struct ZoneConfig { |
804 | | pub name: String, |
805 | | #[serde(rename = "type")] |
806 | | pub kind: ZoneType, |
807 | | #[serde(default)] |
808 | | pub file: Option<String>, |
809 | | #[serde(default)] |
810 | | pub notify: Option<bool>, |
811 | | #[serde(default)] |
812 | | pub notify_acl: Option<String>, |
813 | | #[serde(default)] |
814 | | pub allow_transfer_acl: Option<String>, |
815 | | #[serde(default)] |
816 | | pub allow_update_acl: Option<String>, |
817 | | #[serde(default)] |
818 | | pub axfr_tsig_key: Option<String>, |
819 | | |
820 | | // Slave-specific |
821 | | #[serde(default)] |
822 | | pub masters: Vec<String>, |
823 | | |
824 | | // Inline zone |
825 | | #[serde(default)] |
826 | | pub inline: Option<bool>, |
827 | | #[serde(default)] |
828 | | pub records: Vec<ZoneRecord>, |
829 | | |
830 | | // Forward-specific |
831 | | #[serde(default)] |
832 | | pub forwarders: Vec<String>, |
833 | | #[serde(default)] |
834 | | pub forward_policy: Option<String>, |
835 | | } |
836 | | |
837 | | impl Default for ZoneConfig { |
838 | 1 | fn default() -> Self { |
839 | 1 | ZoneConfig { |
840 | 1 | name: String::new(), |
841 | 1 | kind: ZoneType::Master, |
842 | 1 | file: None, |
843 | 1 | notify: Some(false), |
844 | 1 | notify_acl: None, |
845 | 1 | allow_transfer_acl: None, |
846 | 1 | allow_update_acl: None, |
847 | 1 | axfr_tsig_key: None, |
848 | 1 | masters: Vec::new(), |
849 | 1 | inline: Some(false), |
850 | 1 | records: Vec::new(), |
851 | 1 | forwarders: Vec::new(), |
852 | 1 | forward_policy: None, |
853 | 1 | } |
854 | 1 | } |
855 | | } |
856 | | |
857 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
858 | | #[serde(rename_all = "lowercase")] |
859 | | #[derive(PartialEq)] |
860 | | pub enum ZoneType { |
861 | | Master, |
862 | | Slave, |
863 | | Forward, |
864 | | Stub, |
865 | | } |
866 | | |
867 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
868 | | pub struct ZoneRecord { |
869 | | pub name: String, |
870 | | pub ttl: Option<u32>, |
871 | | pub class: Option<String>, |
872 | | #[serde(rename = "type")] |
873 | | pub r#type: String, |
874 | | pub rdata: String, |
875 | | #[serde(default)] |
876 | | pub priority: Option<u16>, |
877 | | } |
878 | | |
879 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
880 | | pub struct TsigKey { |
881 | | pub name: String, |
882 | | pub algorithm: String, |
883 | | pub secret: String, // TODO: base64 encoded - do not keep in plaintext in production |
884 | | } |
885 | | |
886 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
887 | | pub struct AxfrConfig { |
888 | | pub enabled: bool, |
889 | | pub max_concurrent_transfers: usize, |
890 | | pub transfer_timeout_secs: u64, |
891 | | } |
892 | | |
893 | | impl Default for AxfrConfig { |
894 | 5 | fn default() -> Self { |
895 | 5 | AxfrConfig { |
896 | 5 | enabled: true, |
897 | 5 | max_concurrent_transfers: 4, |
898 | 5 | transfer_timeout_secs: 120, |
899 | 5 | } |
900 | 5 | } |
901 | | } |
902 | | |
903 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
904 | | pub struct DnssecConfig { |
905 | | pub enabled: bool, |
906 | | pub auto_sign: bool, |
907 | | pub default_algo: String, |
908 | | pub kasp_file: Option<String>, |
909 | | } |
910 | | |
911 | | impl Default for DnssecConfig { |
912 | 5 | fn default() -> Self { |
913 | 5 | DnssecConfig { |
914 | 5 | enabled: false, |
915 | 5 | auto_sign: false, |
916 | 5 | default_algo: "RSASHA256".to_string(), |
917 | 5 | kasp_file: None, |
918 | 5 | } |
919 | 5 | } |
920 | | } |
921 | | |
922 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
923 | | pub struct PolicyConfig { |
924 | | #[serde(default)] |
925 | | pub deny_domains: Vec<String>, |
926 | | } |
927 | | |
928 | | impl Default for PolicyConfig { |
929 | 4 | fn default() -> Self { |
930 | 4 | PolicyConfig { |
931 | 4 | deny_domains: Vec::new(), |
932 | 4 | } |
933 | 4 | } |
934 | | } |
935 | | |
936 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
937 | | pub struct AmplificationMitigationConfig { |
938 | | pub drop_fragments: bool, |
939 | | pub max_response_size_udp: usize, |
940 | | } |
941 | | |
942 | | impl Default for AmplificationMitigationConfig { |
943 | 4 | fn default() -> Self { |
944 | 4 | AmplificationMitigationConfig { |
945 | 4 | drop_fragments: true, |
946 | 4 | max_response_size_udp: 4096, |
947 | 4 | } |
948 | 4 | } |
949 | | } |
950 | | |
951 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
952 | | pub struct TuningConfig { |
953 | | pub socket_recv_buffer_bytes: usize, |
954 | | pub socket_send_buffer_bytes: usize, |
955 | | pub max_label_length: usize, |
956 | | pub max_domain_length: usize, |
957 | | } |
958 | | |
959 | | impl Default for TuningConfig { |
960 | 4 | fn default() -> Self { |
961 | 4 | TuningConfig { |
962 | 4 | socket_recv_buffer_bytes: 262_144, |
963 | 4 | socket_send_buffer_bytes: 262_144, |
964 | 4 | max_label_length: 63, |
965 | 4 | max_domain_length: 253, |
966 | 4 | } |
967 | 4 | } |
968 | | } |
969 | | |
970 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
971 | | pub struct ViewConfig { |
972 | | pub name: String, |
973 | | pub acl: String, |
974 | | #[serde(default)] |
975 | | pub zones: Vec<ViewZone>, |
976 | | } |
977 | | |
978 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
979 | | pub struct ViewZone { |
980 | | pub name: String, |
981 | | pub file: String, |
982 | | } |
983 | | |
984 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
985 | | pub struct MonitoringConfig { |
986 | | pub enable_query_logging: bool, |
987 | | pub query_log_path: String, |
988 | | pub log_query_qps: u64, |
989 | | } |
990 | | |
991 | | impl Default for MonitoringConfig { |
992 | 4 | fn default() -> Self { |
993 | 4 | MonitoringConfig { |
994 | 4 | enable_query_logging: false, |
995 | 4 | query_log_path: "/var/log/scloud-dns/queries.log".to_string(), |
996 | 4 | log_query_qps: 1000, |
997 | 4 | } |
998 | 4 | } |
999 | | } |
1000 | | |
1001 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
1002 | | pub struct DynUpdateConfig { |
1003 | | pub zone: String, |
1004 | | pub acl: String, |
1005 | | pub tsig_key: Option<String>, |
1006 | | pub allow: bool, |
1007 | | } |
1008 | | |
1009 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
1010 | | pub struct LimitsConfig { |
1011 | | pub max_udp_packet_size: usize, |
1012 | | pub max_queries_per_minute_per_ip: u64, |
1013 | | pub max_tcp_sessions_per_ip: usize, |
1014 | | } |
1015 | | |
1016 | | impl Default for LimitsConfig { |
1017 | 5 | fn default() -> Self { |
1018 | 5 | LimitsConfig { |
1019 | 5 | max_udp_packet_size: 4096, |
1020 | 5 | max_queries_per_minute_per_ip: 1000, |
1021 | 5 | max_tcp_sessions_per_ip: 8, |
1022 | 5 | } |
1023 | 5 | } |
1024 | | } |